"""
doc string comes here
"""

from math import log
from random import randint as randi
from random import random
from random import uniform as rand
from numpy import shape, array

algorithmSettings = {'chromosomeLength' : [3],
                     'populationSize' : 10,
                     'iterations' : 2000,
                     'crossoverRate' : 0.8,
                     'mutationRate' : 0.01, 
                     'integerProblem' : 'Yes', 
                     'state':'maximize'}

searchSpace = '0,7;0,7'
negativeValues = []

def setChromosomeLength(searchSpace):
    searchSpace = [float(endPoint) for varLimits in searchSpace.split(';') for endPoint in varLimits.split(',')]

    chromosomeLength = []
    for varPointer in range(0,  len(searchSpace), 2):
        lBound = min(searchSpace[varPointer], searchSpace[varPointer+1])
        uBound = max(searchSpace[varPointer], searchSpace[varPointer+1])
        
        isNegative = 0
    
        if lBound<0:
            isNegative = 1
            negativeValues.append('Yes')
        if lBound >=0:
            negativeValues.append('No')
        if lBound ==float('-inf') or uBound == float('inf'):
            chromosomeLength.append(32 + 1*isNegative)
            continue
    
        chromosomeLength.append(int(log(max(abs(lBound), abs(uBound)),  2) + 1 + 1*isNegative))
    
    algorithmSettings['chromosomeLength'] = chromosomeLength


def initChromosome():
    chromosome = [randi(0,1) for i in range(sum(algorithmSettings['chromosomeLength']))]
    return chromosome

def initPopulation():
    population = []
    for member in range(algorithmSettings['populationSize']):
        population.append(initChromosome())
    return population

def crossover(population):
    shouldCrossover = random()
    if shouldCrossover <= algorithmSettings['crossoverRate']:
        for i in range(0,shape(population)[0]-1,2):
            crossoverPoint = randi(0,shape(population)[1]-1)
            #print "Crossing over is taking place at", crossoverPoint
            
            motherChromosome = list(population[i])
            fatherChromosome = list(population[i+1])
            motherChromosome[0:crossoverPoint], fatherChromosome[0:crossoverPoint] = fatherChromosome[0:crossoverPoint], motherChromosome[0:crossoverPoint]
            population[i] = array(motherChromosome)
            population[i+1] = array(fatherChromosome)
    return population

def mutate(population):
    shouldMutate = random()
    if shouldMutate <= algorithmSettings['mutationRate']:
        mutateMember = randi(0,shape(population)[0]-1)
        mutateBit = randi(0,shape(population)[1]-1)
        population[mutateMember][mutateBit] = ((population[mutateMember][mutateBit])+1)%2
    return population

def splitMember(member,  chromosomeLength):
    splittedMember = []
    start = 0
    for cLength in chromosomeLength:
        end = start+ cLength
        currentGCode = member[start:end]
        bitString = ''
        for bit in currentGCode:
            bitString += str(bit)
        splittedMember.append(bitString)
        start = end 
    return splittedMember

def evaluateVariableValues(population):
    chromosomeLength = algorithmSettings['chromosomeLength']
    values = []
    for member in population:
        memberValues = []
        variableGCodes = splitMember(member,  chromosomeLength)
        variableIndex = 0
        for bitString in variableGCodes:
            if negativeValues[variableIndex] == 'Yes':
                memberValues.append((int(bitString[1:],  2)) * ((-1)**int(bitString[0])))
            else:
                memberValues.append(int(bitString,  2))
            variableIndex += 1
        values.append(memberValues)
    return values

def normalizeFitness(fitnessValues):
    fitnessMax = max(fitnessValues)
    fitnessMin = min(fitnessValues)
    
    newFitness = [1+fitness+abs(fitnessMin) for fitness in fitnessValues]
    fitnessValues = [float(fitness)*100/max(newFitness) for fitness in newFitness]
    return fitnessValues

def evaluateFitness(population,  objectiveFunctions,  constraintFunctions,  variables):
    constraintFunctions = constraintFunctions.split(';')
    varValues = evaluateVariableValues(population)
    fitnessValues = []
    memberCount = 0
    variables = variables.split()
    for member in population:
        parameterValues = {}
        for value, symbol in zip(varValues[memberCount],  variables):
            parameterValues[symbol] = value
        fitness = eval(objectiveFunctions,  parameterValues)

        for function in constraintFunctions:
            if function != '' and not eval(function,  parameterValues):
                if algorithmSettings['state'] =='maximize':
                    fitness -= 10000
                else:
                    fitness += 10000
        
        fitnessValues.append(fitness)
        memberCount += 1
        normalizedFitnessValues = normalizeFitness(fitnessValues)
    return normalizedFitnessValues
    
def sortChromosomes(population,  fitnessValues):
    augmentedChromosome = {}
    for member, value in zip(population,  fitnessValues):
        augmentedChromosome[value] = member
    return augmentedChromosome.values()

def rouletteWheel(population,  fitnessValues):
    n = len(fitnessValues)
    if sum(fitnessValues) != 0:
        avgFitness = [float(fitness)/sum(fitnessValues) for fitness in fitnessValues]
    else:
        avgFitness = [float(fitness)/(sum(fitnessValues)+1) for fitness in fitnessValues]    
    reproductionPool = []
    for member in range(n):
        probability = rand(0, 1)
        memberID = 0
        partialSum = 0
        while probability > partialSum:
            partialSum += avgFitness[memberID]
            memberID +=1
            if memberID >= algorithmSettings['populationSize']:
                memberID = algorithmSettings['populationSize']-1
        reproductionPool.append(population[memberID])
    return reproductionPool
        
def optimize(variables,  objectiveFunctions,  constraintFunctions):
    setChromosomeLength(searchSpace)
    population = initPopulation()
    
    for iterCount in range(algorithmSettings['iterations']):
        fitnessValues = evaluateFitness(population,  objectiveFunctions,  constraintFunctions,  variables)
        population = rouletteWheel(population, fitnessValues)
        if algorithmSettings['state'] == 'maximize':
            population.reverse()
        population = mutate(crossover(population))
        print 'Iteration no. = ', iterCount
    return evaluateVariableValues(population)

##if __name__ =="__main__":
##    Variables = ['x']
##    Objective = 'x**2-8*x+17'
##    Constraints = ['x<100']
##
##    searchSpace = '0,7'
##    algorithmSettings['iterations'] = 100000
##    algorithmSettings['populationSize'] = 2
##    algorithmSettings['state'] = 'minimize'
##    finalSolution =  optimize(Variables, Objective, Constraints)
##    print evaluateVariableValues(finalSolution)
